package main

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"rabbit/build"
	"rabbit/cmd"
	"rabbit/database"
	db "rabbit/database"
	"runtime"
	"strconv"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/dgrijalva/jwt-go/request"

	"github.com/erikdubbelboer/gspt"
	"github.com/gin-gonic/gin"
	"github.com/urfave/cli"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo/options"
)

const (
	BASE_UID = 10001
)

const (
	MONGODB_ERR_NOFOUND = "mongo: no documents in result"
)

const (
	JWT_TOKENKEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()"
)

type Server struct {
	DataDir     string
	BindAddress string
	HTTPSPort   int

	Token         string
	TokenFile     string
	ClusterSecret string
}

var (
	ServerConfig Server
)

type loginResponse struct {
	Uid   uint64 `json:"uid"` // 账户id
	Token string `json:"token"`
}

func user_exists(uid uint64) bool {
	filter := bson.M{"uid": uid}
	user := &database.UserInfo{}
	if err := db.GetOne(user, filter); err != nil {
		return false
	}

	return true
}

// 用户注册/登录/更新用户信息,维护用户在线状态
// 行消息推送
var runCmd = cli.Command{
	Name:            "run",
	ShortName:       "",
	Usage:           "Start a rabbit login server",
	SkipFlagParsing: true,
	SkipArgReorder:  true,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:        "bind-address",
			Usage:       "bind address (default: 0.0.0.0)",
			Value:       "0.0.0.0",
			Destination: &ServerConfig.BindAddress,
		},
		cli.IntFlag{
			Name:        "https-listen-port",
			Usage:       "(listener) HTTPS listen port",
			Value:       8082,
			Destination: &ServerConfig.HTTPSPort,
		},
	},
	Action: func(*cli.Context) error {
		fmt.Println("rabbit login server ready!")

		go func() {
			fmt.Println("statistical user online status")
			for {
				time.Sleep(time.Second * 10)
			}
		}()

		router := gin.Default()
		router.POST("/register", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_REGISTER

			body, err := c.GetRawData()
			if err != nil {
				rimres.ErrMsg = "nil body data"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			user := &database.UserInfo{}
			err = json.Unmarshal(body, user)
			if err != nil {
				rimres.ErrMsg = "body data not json format"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			// fill default attr
			user.FaceType = 0
			user.Birthday = time.Date(2000, 2, 4, 10, 30, 0, 0, time.Local)
			user.Gender = 0
			rand.Seed(time.Now().UnixNano())
			user.FaceType = uint8(rand.Intn(35))
			user.UpdateTime = time.Now()

			// find if user exists?
			filter := bson.M{"username": user.UserName}
			userTemp := &database.UserInfo{}
			if err := db.GetOne(userTemp, filter); err != nil && err.Error() != MONGODB_ERR_NOFOUND {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			if userTemp.Uid != 0 {
				rimres.ErrMsg = "user already exists!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			// apply uid
			if collection, err := user.GetCollecton(); err == nil {
				rst := collection.FindOne(context.TODO(), bson.D{}, options.FindOne().SetSort(bson.D{{"uid", -1}}))
				if err := rst.Decode(userTemp); err == nil {
					user.Uid = userTemp.Uid + 1
				}
			}
			if user.Uid == 0 {
				user.Uid = BASE_UID
			}
			if _, err := db.AddOne(user); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			rimres.StatusCode = 0
			rimres.CmdBody = user
			c.JSON(http.StatusOK, rimres)
		})
		router.POST("/login/:name/:passwd", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_LOGIN

			name := c.Param("name")
			passwd := c.Param("passwd")
			if name == "" || passwd == "" {
				rimres.ErrMsg = "username or password null!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			userTemp := &database.UserInfo{}
			filter := bson.M{"username": name}
			if err := db.GetOne(userTemp, filter); err != nil {
				rimres.ErrMsg = "user not exists!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			if userTemp.Uid == 0 || userTemp.UserName != name || userTemp.Password != passwd {
				rimres.ErrMsg = "username or password error!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			claims := make(jwt.MapClaims)
			claims["name"] = name
			claims["iat"] = time.Now().Unix()
			claims["exp"] = time.Now().Add(time.Hour * time.Duration(1)).Unix()
			tokenstr, _ := GenerateToken(claims, JWT_TOKENKEY)

			filter = bson.M{"uid": userTemp.Uid}
			userToken := &database.UserToken{Uid: userTemp.Uid, Token: tokenstr}
			if err := db.Update(userToken, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			loginres := loginResponse{Token: userToken.Token, Uid: userTemp.Uid}
			rimres.StatusCode = 0
			rimres.CmdBody = loginres
			c.JSON(http.StatusOK, rimres)
		})
		router.POST("/update/:uid/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_UPDATE

			body, err := c.GetRawData()
			if err != nil {
				rimres.ErrMsg = "nil body data"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			userTemp := &database.UserInfo{}
			err = json.Unmarshal(body, userTemp)
			if err != nil {
				rimres.ErrMsg = "body data not json format"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			filter := bson.M{"username": userTemp.UserName}
			user := &database.UserInfo{}
			if err := db.GetOne(user, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			// check token

			if userTemp.Address != "" {
				user.Address = userTemp.Address
			}
			if userTemp.NickName != "" {
				user.NickName = userTemp.NickName
			}
			if userTemp.FaceType != 0 {
				user.FaceType = userTemp.FaceType
			}
			if userTemp.CustomFace != "" {
				user.CustomFace = userTemp.CustomFace
			}
			if userTemp.CustomFaceFmt != 0 {
				user.CustomFaceFmt = userTemp.CustomFaceFmt
			}
			if userTemp.Gender != 0 {
				user.Gender = userTemp.Gender
			}
			if userTemp.Age != 0 {
				user.Age = userTemp.Age
			}
			if userTemp.Height != 0 {
				user.Height = userTemp.Height
			}
			user.Birthday = userTemp.Birthday

			if _, err := db.AddOne(user); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			rimres.StatusCode = 0
			rimres.CmdBody = user
			c.JSON(http.StatusOK, rimres)
		})
		router.GET("/userlist/:uid/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_USERLIST

			uid, _ := strconv.ParseInt(c.Param("uid"), 10, 64)
			//token := c.Param("token")

			// check token

			filter := bson.M{"uid": uid}
			user := &database.UserInfo{}
			if err := db.GetOne(user, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			rimres.StatusCode = 0
			rimres.CmdBody = user.Friends
			c.JSON(http.StatusOK, rimres)
		})
		router.GET("/grouplist/:uid/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_GROUPLIST

			uid, _ := strconv.ParseInt(c.Param("uid"), 10, 64)
			//token := c.Param("token")

			// check token

			filter := bson.M{"uid": uid}
			user := &database.UserInfo{}
			if err := db.GetOne(user, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			groups := []database.UserGroup{}
			gids := []uint64{}
			gids = append(gids, user.OwnerGids...)
			gids = append(gids, user.JoinGids...)
			for _, gid := range gids {
				filter := bson.M{"gid": gid}
				group := database.UserGroup{}
				if err := db.GetOne(group, filter); err == nil {
					groups = append(groups, group)
				}
			}

			rimres.StatusCode = 0
			rimres.CmdBody = groups
			c.JSON(http.StatusOK, rimres)
		})
		router.POST("/addfriend/:uidsrc/:uiddst/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_ADDFRIEND

			uidsrc, _ := strconv.ParseInt(c.Param("uidsrc"), 10, 64)
			uiddst, _ := strconv.ParseInt(c.Param("uiddst"), 10, 64)
			uidsmall := uidsrc
			uidbig := uiddst
			if uidsmall > uidbig {
				uidsmall, uidbig = uidbig, uidsmall
			}

			//token := c.Param("token")

			// check token

			if !user_exists(uint64(uidsrc)) || !user_exists(uint64(uiddst)) {
				rimres.ErrMsg = "uidsrc or uiddst not exists!"
				c.JSON(http.StatusOK, rimres)
				return
			}

			filter := bson.M{"uidsmall": uidsmall, "uidbig": uidbig}
			userRelationshipTmp := &database.UserRelationship{}
			if err := db.GetOne(userRelationshipTmp, filter); err != nil && err.Error() != MONGODB_ERR_NOFOUND {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			if userRelationshipTmp.Fixed == true || (userRelationshipTmp.UidSmall == uint64(uidsmall) && userRelationshipTmp.UidBig == uint64(uidbig)) {
				rimres.StatusCode = 1
				rimres.ErrMsg = "already friends!"
				c.JSON(http.StatusOK, rimres)
				return
			}

			userRelationship := &database.UserRelationship{
				UidSubmit:  uint64(uidsrc),
				UidGrantee: uint64(uiddst),
				UidSmall:   uint64(uidsmall),
				UidBig:     uint64(uidbig),
				UpdateTime: time.Now(),
			}
			if _, err := db.AddOne(userRelationship); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			rimres.StatusCode = 0
			c.JSON(http.StatusOK, rimres)
		})
		router.GET("/newfriendsapplyforlist/:uid/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_NEWFRIEND_APPLY_FOR_LIST

			uid, _ := strconv.ParseInt(c.Param("uid"), 10, 64)

			//token := c.Param("token")

			// check token

			filter := bson.M{"fixed": false, "uidgrantee": uid}
			userRelationshipTmp := &database.UserRelationship{}
			userRelationships := []database.UserRelationship{}
			if err := db.GetList(userRelationshipTmp, filter, userRelationships); err != nil && err.Error() != MONGODB_ERR_NOFOUND {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			rimres.StatusCode = 0
			rimres.CmdBody = userRelationships
			c.JSON(http.StatusOK, rimres)
		})
		router.POST("/confirmmakefriend/:uidsrc/:uiddst/:token", func(c *gin.Context) {
			rimres := cmd.RimResponse{StatusCode: -1}
			rimres.RestApi = cmd.RESTAPI_CONFIRM_MAKE_FRIEND

			uidsrc, _ := strconv.ParseInt(c.Param("uidsrc"), 10, 64)
			uiddst, _ := strconv.ParseInt(c.Param("uiddst"), 10, 64)
			uidsmall := uidsrc
			uidbig := uiddst
			if uidsmall > uidbig {
				uidsmall, uidbig = uidbig, uidsmall
			}

			//token := c.Param("token")

			// check token

			filter := bson.M{"fixed": true, "uidsmall": uidsmall, "uidbig": uidbig}
			userRelationshipTmp := &database.UserRelationship{}
			if err := db.GetOne(userRelationshipTmp, filter); err != nil && err.Error() != MONGODB_ERR_NOFOUND {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			if userRelationshipTmp.Fixed == true && userRelationshipTmp.UidSmall == uint64(uidsmall) && userRelationshipTmp.UidBig == uint64(uidbig) {
				rimres.ErrMsg = "already friends!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			if uint64(uidsrc) != userRelationshipTmp.UidGrantee || uint64(uiddst) != userRelationshipTmp.UidSubmit {
				rimres.ErrMsg = "parameter invalid!"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			// update relevant tables.
			userRelationship := &database.UserRelationship{
				Fixed:      true,
				UidSubmit:  uint64(uiddst),
				UidGrantee: uint64(uidsrc),
				UidSmall:   uint64(uidsmall),
				UidBig:     uint64(uidbig),
				UpdateTime: time.Now(),
			}
			if _, err := db.AddOne(userRelationship); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			filter = bson.M{"uid": uidsrc}
			userTemp := &database.UserInfo{}
			if err := db.GetOne(userTemp, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			userTemp.Friends = append(userTemp.Friends, &database.FriendInfo{Friendid: uint64(uiddst)})
			if _, err := db.AddOne(userTemp); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			filter = bson.M{"uid": uiddst}
			userTemp = &database.UserInfo{}
			if err := db.GetOne(userTemp, filter); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}
			userTemp.Friends = append(userTemp.Friends, &database.FriendInfo{Friendid: uint64(uidsrc)})
			if _, err := db.AddOne(userTemp); err != nil {
				rimres.ErrMsg = "database internal error"
				c.JSON(http.StatusBadRequest, rimres)
				return
			}

			// notice event to user uiddst by pusher

			rimres.StatusCode = 0
			c.JSON(http.StatusOK, rimres)
		})

		router.Run(ServerConfig.BindAddress + fmt.Sprintf(":%d", ServerConfig.HTTPSPort))

		return nil
	},
}

func main() {
	// hide process arguments from ps output, since they may contain
	// database credentials or other secrets.
	gspt.SetProcTitle("rabbit " + os.Args[0])

	app := cli.NewApp()
	app.Name = build.AppName
	app.Usage = "rabbit im, the lightweight real-time communication tools. login server"
	app.Version = fmt.Sprintf("%s (%s)", build.Version, build.GitCommit)
	app.Author = "tang shun cai"
	app.Commands = []cli.Command{
		runCmd,
	}

	cli.VersionPrinter = func(c *cli.Context) {
		fmt.Printf("%s version %s\n", app.Name, app.Version)
		fmt.Printf("go version %s\n", runtime.Version())
	}
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name:  "data-dir,d",
			Usage: "(data) Folder to hold state default /var/lib/rabbit/data/" + build.Program + " or ${HOME}/.rabbit/" + build.Program + " if not root",
		},
	}

	if err := app.Run(os.Args); err != nil {
		fmt.Println(err)
	}
}

func GenerateToken(mapClaims jwt.MapClaims, key string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	return token.SignedString([]byte(key))
}

// token example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2UiOjE4LCJuYW1lIjoieHh4eCJ9.M0dhViqnOmAV0kSpCtYIysSIBubRYq2hkRfujhNG9ko
func ParseToken(token string, secret string) (string, error) {
	claim, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret), nil
	})
	if err != nil {
		return "", err
	}
	return claim.Claims.(jwt.MapClaims)["name"].(string), nil
}

func ValidateTokenMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
		return []byte(JWT_TOKENKEY), nil
	})

	if err == nil {
		if token.Valid {
			next(w, r)
		} else {
			w.WriteHeader(http.StatusUnauthorized)
			fmt.Fprint(w, "Token is not valid")
		}
	} else {
		w.WriteHeader(http.StatusUnauthorized)
		fmt.Fprint(w, "Unauthorized access to this resource")
	}
}
